// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of OpenEthereum.

// OpenEthereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// OpenEthereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with OpenEthereum.  If not, see <http://www.gnu.org/licenses/>.

//! On-chain randomness generation for authority round
//!
//! This module contains the support code for the on-chain randomness generation used by AuRa. Its
//! core is the finite state machine `RandomnessPhase`, which can be loaded from the blockchain
//! state, then asked to perform potentially necessary transaction afterwards using the `advance()`
//! method.
//!
//! No additional state is kept inside the `RandomnessPhase`, it must be passed in each time.
//!
//! The process of generating random numbers is a simple finite state machine:
//!
//! ```text
//!                                                       +
//!                                                       |
//!                                                       |
//!                                                       |
//! +--------------+                              +-------v-------+
//! |              |                              |               |
//! | BeforeCommit <------------------------------+    Waiting    |
//! |              |          enter commit phase  |               |
//! +------+-------+                              +-------^-------+
//!        |                                              |
//!        |  call                                        |
//!        |  `commitHash()`                              |  call
//!        |                                              |  `revealNumber()`
//!        |                                              |
//! +------v-------+                              +-------+-------+
//! |              |                              |               |
//! |  Committed   +------------------------------>    Reveal     |
//! |              |  enter reveal phase          |               |
//! +--------------+                              +---------------+
//! ```
//!
//! Phase transitions are performed by the smart contract and simply queried by the engine.
//!
//! Randomness generation works as follows:
//! * During the commit phase, all validators locally generate a random number, and commit that number's hash to the
//!   contract.
//! * During the reveal phase, all validators reveal their local random number to the contract. The contract should
//!   verify that it matches the committed hash.
//! * Finally, the XOR of all revealed numbers is used as an on-chain random number.
//!
//! An adversary can only influence that number by either controlling _all_ validators who committed, or, to a lesser
//! extent, by not revealing committed numbers.
//! The length of the commit and reveal phases, as well as any penalties for failure to reveal, are defined by the
//! contract.
//!
//! A typical case of using `RandomnessPhase` is:
//!
//! 1. `RandomnessPhase::load()` the phase from the blockchain data.
//! 2. Call `RandomnessPhase::advance()`.
//!
//! A production implementation of a randomness contract can be found here:
//! https://github.com/poanetwork/posdao-contracts/blob/4fddb108993d4962951717b49222327f3d94275b/contracts/RandomAuRa.sol

use bytes::Bytes;
use crypto::publickey::{ecies, Error as CryptoError};
use derive_more::Display;
use engines::signer::EngineSigner;
use ethabi::Hash;
use ethabi_contract::use_contract;
use ethereum_types::{Address, H256, U256};
use hash::keccak;
use log::{debug, error};
use rand::Rng;

use super::util::{BoundContract, CallError};

/// Random number type expected by the contract: This is generated locally, kept secret during the commit phase, and
/// published in the reveal phase.
pub type RandNumber = H256;

use_contract!(aura_random, "res/contracts/authority_round_random.json");

/// Validated randomness phase state.
#[derive(Debug)]
pub enum RandomnessPhase {
    // NOTE: Some states include information already gathered during `load` (e.g. `our_address`,
    //       `round`) for efficiency reasons.
    /// Waiting for the next phase.
    ///
    /// This state indicates either the successful revelation in this round or having missed the
    /// window to make a commitment, i.e. having failed to commit during the commit phase.
    Waiting,
    /// Indicates a commitment is possible, but still missing.
    BeforeCommit,
    /// Indicates a successful commitment, waiting for the commit phase to end.
    Committed,
    /// Indicates revealing is expected as the next step.
    Reveal { our_address: Address, round: U256 },
}

/// Phase loading error for randomness generation state machine.
///
/// This error usually indicates a bug in either the smart contract, the phase loading function or
/// some state being lost.
///
/// `BadRandNumber` will usually result in punishment by the contract or the other validators.
#[derive(Debug, Display)]
pub enum PhaseError {
    /// The smart contract reported that we already revealed something while still being in the
    /// commit phase.
    #[display(fmt = "Revealed during commit phase")]
    RevealedInCommit,
    /// Failed to load contract information.
    #[display(fmt = "Error loading randomness contract information: {:?}", _0)]
    LoadFailed(CallError),
    /// Failed to load the stored encrypted random number.
    #[display(fmt = "Failed to load random number from the randomness contract")]
    BadRandNumber,
    /// Failed to encrypt random number.
    #[display(fmt = "Failed to encrypt random number: {}", _0)]
    Crypto(CryptoError),
    /// Failed to get the engine signer's public key.
    #[display(fmt = "Failed to get the engine signer's public key")]
    MissingPublicKey,
}

impl From<CryptoError> for PhaseError {
    fn from(err: CryptoError) -> PhaseError {
        PhaseError::Crypto(err)
    }
}

impl RandomnessPhase {
    /// Determine randomness generation state from the contract.
    ///
    /// Calls various constant contract functions to determine the precise state that needs to be
    /// handled (that is, the phase and whether or not the current validator still needs to send
    /// commitments or reveal random numbers).
    pub fn load(
        contract: &BoundContract,
        our_address: Address,
    ) -> Result<RandomnessPhase, PhaseError> {
        // Determine the current round and which phase we are in.
        let round = contract
            .call_const(aura_random::functions::current_collect_round::call())
            .map_err(PhaseError::LoadFailed)?;
        let is_commit_phase = contract
            .call_const(aura_random::functions::is_commit_phase::call())
            .map_err(PhaseError::LoadFailed)?;

        // Ensure we are not committing or revealing twice.
        let committed = contract
            .call_const(aura_random::functions::is_committed::call(
                round,
                our_address,
            ))
            .map_err(PhaseError::LoadFailed)?;
        let revealed: bool = contract
            .call_const(aura_random::functions::sent_reveal::call(
                round,
                our_address,
            ))
            .map_err(PhaseError::LoadFailed)?;

        // With all the information known, we can determine the actual state we are in.
        if is_commit_phase {
            if revealed {
                return Err(PhaseError::RevealedInCommit);
            }

            if !committed {
                Ok(RandomnessPhase::BeforeCommit)
            } else {
                Ok(RandomnessPhase::Committed)
            }
        } else {
            if !committed {
                // We apparently entered too late to make a commitment, wait until we get a chance again.
                return Ok(RandomnessPhase::Waiting);
            }

            if !revealed {
                Ok(RandomnessPhase::Reveal { our_address, round })
            } else {
                Ok(RandomnessPhase::Waiting)
            }
        }
    }

    /// Advance the random seed construction process as far as possible.
    ///
    /// Returns the encoded contract call necessary to advance the randomness contract's state.
    ///
    /// **Warning**: After calling the `advance()` function, wait until the returned transaction has been included in
    /// a block before calling it again; otherwise spurious transactions resulting in punishments might be executed.
    pub fn advance<R: Rng>(
        self,
        contract: &BoundContract,
        rng: &mut R,
        signer: &dyn EngineSigner,
    ) -> Result<Option<Bytes>, PhaseError> {
        match self {
            RandomnessPhase::Waiting | RandomnessPhase::Committed => Ok(None),
            RandomnessPhase::BeforeCommit => {
                // Generate a new random number, but don't reveal it yet. Instead, we publish its hash to the
                // randomness contract, together with the number encrypted to ourselves. That way we will later be
                // able to decrypt and reveal it, and other parties are able to verify it against the hash.
                let number: RandNumber = rng.gen();
                let number_hash: Hash = keccak(number.0);
                let public = signer.public().ok_or(PhaseError::MissingPublicKey)?;
                let cipher = ecies::encrypt(&public, number_hash.as_bytes(), number.as_bytes())?;

                debug!(target: "engine", "Randomness contract: committing {}.", number_hash);
                // Return the call data for the transaction that commits the hash and the encrypted number.
                let (data, _decoder) =
                    aura_random::functions::commit_hash::call(number_hash, cipher);
                Ok(Some(data))
            }
            RandomnessPhase::Reveal { round, our_address } => {
                // Load the hash and encrypted number that we stored in the commit phase.
                let call = aura_random::functions::get_commit_and_cipher::call(round, our_address);
                let (committed_hash, cipher) =
                    contract.call_const(call).map_err(PhaseError::LoadFailed)?;

                // Decrypt the number and check against the hash.
                let number_bytes = signer.decrypt(&committed_hash.0, &cipher)?;
                let number = if number_bytes.len() == 32 {
                    RandNumber::from_slice(&number_bytes)
                } else {
                    // This can only happen if there is a bug in the smart contract,
                    // or if the entire network goes awry.
                    error!(target: "engine", "Decrypted random number has the wrong length.");
                    return Err(PhaseError::BadRandNumber);
                };
                let number_hash: Hash = keccak(number.0);
                if number_hash != committed_hash {
                    error!(target: "engine", "Decrypted random number doesn't agree with the hash.");
                    return Err(PhaseError::BadRandNumber);
                }

                debug!(target: "engine", "Randomness contract: scheduling tx to reveal our random number {} (round={}, our_address={}).", number_hash, round, our_address);
                // We are now sure that we have the correct secret and can reveal it. So we return the call data for the
                // transaction that stores the revealed random bytes on the contract.
                let (data, _decoder) = aura_random::functions::reveal_number::call(number.0);
                Ok(Some(data))
            }
        }
    }
}
